/*
 * Title: WADS
 * Description: Web-based Auto flow Documents System
 * Copyright: 2007
 * Company: CoE
 * @author: Mr.Nattasan Wang-Aphisit
 * @version 1.1
 */

package wads.drawing.line;

import wads.app.*;
import wads.drawing.CaseShape;
import wads.drawing.node.CaseNode;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;

public abstract class CaseLink extends CaseShape implements Cloneable, Serializable{
  //Coordinates of Start Point
  private int x1, y1;

  //Coordinates of End Point
  private int x2, y2;

  //References to the node that the link start and ends
  private CaseNode startNode, endNode;

  private Point moveAnchor;

  //Flag to indicate if link is confirmed ie. solidly drawn
  private boolean linkConfirmed = false;

  private String linkLabel;
  private transient CaseLinkDialog linkDialog;  //a Dialog/OptionPane
  //private transient CaseLinkDialogOld linkDialog;
  
  private static final float LINK_DOTTED_DASH[] = {4.0f};
  private static final BasicStroke LINK_DOTTED_STROKE = new BasicStroke(1.0f,
      BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, LINK_DOTTED_DASH, 0.0f);

  private transient BasicStroke currentStroke;
  private transient SelectionHandle selectionHandles[];

  /** Overrides in a subclass to define the drawing of the link */
  protected abstract void createLink(int x1, int y1, int x2, int y2);

  //To determine if link is defined about origin and thus needs
	//to transform from origin to designated location
  public abstract boolean needsTransform();

  // cardinality at start and end node
  private String cardinalityAtStartNode;
  private String cardinalityAtEndNode;

  public CaseLink(int x1, int y1, int x2, int y2){
    super();
    createLink(x1, y1, x2, y2);
    setLinkShapeBounds2D(x1, y1, x2, y2);
    setCurrentStroke(getDefaultStroke());
    //linkDialog = new CaseLinkDialog();
    cardinalityAtStartNode = "";
    cardinalityAtEndNode = "";
  }

  public double getDistance(Point p1, Point p2){
    //Compute Distance between the start and end points
    return getDistance(p1.x, p1.y, p2.x, p2.y);
  }

  public double getDistance(int x1, int y1, int x2, int y2){
    //Compute Distance between the start and end points
    return Math.sqrt(Math.pow((y1 - y2), 2) + Math.pow((x1-x2), 2));
  }

  public void initLink(int x1, int y1, int x2, int y2){
    //Remove all previously defined components
    removeAllComponents();

    //save specified coordinates of link
    setLink(x1, y1, x2, y2);

    //set bounds of link
    setLinkShapeBounds2D(x1, y1, x2, y2);
  }

  //A wrapper to the actual create link method
  //which have a extra parameter to determine a need to add this link
  //to the nodes.
  public void createLink(CaseNode nodeAnchored, CaseNode nodeEnded){
    createLink(nodeAnchored, nodeEnded, true);
  }

  protected void createLink(CaseNode nodeAnchored, CaseNode nodeEnded, boolean addToNodes){
    //Check that the anchored and ended nodes are different
    if ((nodeAnchored != null) && (nodeAnchored != nodeEnded)){
      //Computes the edgepoints of the link
      Point startPoint = getEdgePoint(nodeAnchored, nodeEnded);
      Point endPoint = getEdgePoint(nodeEnded, nodeAnchored);

      //Keep the references to the nodes
            setStartNode(nodeAnchored);
            setEndNode(nodeEnded);

      if(addToNodes) {
        //Add the link to the connected nodes
                getStartNode().addConnectedLink(this);
                getEndNode().addConnectedLink(this);
      }

      //"Confirms" the link so that it can now be drawn with a solid line
      confirmLink(startPoint, endPoint);
    }
  }
  
  private void createTmpLink(int x1, int y1, int x2, int y2){
    /** The first 3 steps are also the standard steps for createLink.
     *  Can we somehow keep them in this class so that the subclass is freed
     *  from typing the same code again ?!!!
     */

    //1. Remove all previously defined components
    removeAllComponents();

    //2. Save specified coordinates of link
    setLink(x1, y1, x2, y2);

    //3. Defines dotted line and add it to the compound shape
    append(new Line2D.Float(x1,y1,x2,y2));

    //Set line type to dotted
    setCurrentStroke(LINK_DOTTED_STROKE);
  }

  /*  Refresh the defination link
   *  One use of this is when the nodes which it is connected are moved.
   *  The start and end point of this link needs to be redefined.
   *  Or anytime when the nodes attached might have changed (eg. resize/moved)
   *  Refresh is not the same as createLink(this.startNode,this.endNode)
   *  as createLink adds references to the nodes.
   */
  public void refresh() {
    createLink(this.getStartNode(), this.getEndNode(), false);
  }

  /* Set the starting and ending pt of the link */
  private void setLink(int x1, int y1, int x2, int y2){
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
  }

  public void setLinkShapeBounds2D(int x1, int y1, int x2, int y2){
    //buffer pixel distance from actual shape
    int boundsDX = 2;
    int boundsDY = 6;

    //Compute distance between the start and end points
    int dist = (int)Math.sqrt(Math.pow(x1-x2,2) + Math.pow(y1-y2,2));

    //Construct bounds about origin
    bounds2D = new Rectangle.Float(-boundsDX, -boundsDY, dist+2*boundsDX, 2*boundsDY);

    //Get required transformation
    AffineTransform at = getTransform(x1, y1, x2, y2);

    //Create bounds at transformed position
    //A GeneralPath will be created, NOT a Rectangle/Rectangle2D
    setShapeBounds2D(at.createTransformedShape(bounds2D));
  }

  @Override
  public Shape getScaledBounds(float scaleFactor){
    //buffer pixel distance from actual shape
    int boundsDX = 2;
    int boundsDY = 6;

    //Compute distance between the start and end points
    int dist = (int)Math.sqrt(Math.pow(x1-x2,2) + Math.pow(y1-y2,2));

    //Construct bounds about origin
    Shape scaledBounds2D = new Rectangle2D.Float(-boundsDX, -boundsDY, dist*scaleFactor+2*boundsDX, 2*boundsDY);

    //Get required transformation
    AffineTransform at = getTransform(x1*scaleFactor, y1*scaleFactor, x2*scaleFactor, y2*scaleFactor);

    //Create bounds at transformed position
    //A GeneralPath will be created, NOT a Rectangle/Rectangle2D
    return at.createTransformedShape(scaledBounds2D);
  }

  private AffineTransform getTransform(double x1, double y1, double x2, double y2){
    double rotation = getRotation(x1, y1, x2, y2);
    //Translate at start point
    AffineTransform at1 = new AffineTransform();
    at1.setToTranslation(x1, y1);

    /* The rotation of the AffineTransform does not seem to
       perform rotation from positive X axis to works positive Y axis
       when the rotation angle is positive. It seems to work the
	     opposite and thus the rotation angle is set to -ve.
       Is this a JDK bug or my bug ? However, this works at the moment.
       Could be a logical bug - should set deltaY/deltaX to
	     absolute value !!!
       Check the bug parade again - seems to be JDK bug again.
    */

    //Rotate and translate link to start point
    AffineTransform at2 = new AffineTransform();
    at2.rotate(-rotation);

    //Combine transformation
    at1.concatenate(at2);

    return at1;
  }

  private double getRotation(double x1, double y1, double x2, double y2){
    //Compute deltaX and deltaY
    double deltaX = x1-x2;
    double deltaY = y1-y2;

    //Basic angle of link
    double theta;

    //Check to prevent division by zero
    if (deltaX == 0)
      theta = Math.PI/2;
		//a vertical link is 90 degree to positive x-axis
    else
      //Compute basic angle
      theta = Math.abs(Math.atan(deltaY/deltaX));
    
		//set deltaY/deltaX to absolute value ?!!!

    //Determine quadrant and
    //compute rotation angle of link
    double rotation;

    if ((x1 <= x2) && (y1 >= y2))  //1st Quadrant
      rotation = theta;
    else if ((x1 >= x2) && (y1 >= y2))  //2nd Quadrant
      rotation = Math.PI - theta;
    else if ((x1 >= x2) && (y1 <= y2))  //3rd Quadrant
      rotation = Math.PI + theta;
    else //((x1 <= x2) && (y1 <= y2))  //4th Quadrant
      rotation = -theta;

    return rotation;
  }

    @Override
  public void draw(DrawPanel drawPanel, Graphics g){
    draw((Graphics2D)g);
  }

    @Override
  public void draw(DrawPanel drawPanel){
    draw((Graphics2D)drawPanel.getGraphics());
  }

  @Override
  protected void draw(Graphics2D g2d){
    /** Draws the shape defined for the link.
        This is the core function of the link.
    */
    if (linkConfirmed == false) {
      /* Draw a dotted line as a link */

      //Set to dotted stroke
      g2d.setStroke(LINK_DOTTED_STROKE);

      //Draw the link
      super.draw(g2d);
    }
    else {
      /** Perform transformation for actual link
          Temporary link do not need transformation because
          only a simple dotted line is drawn.
      */

      //Obtain original AffineTransform
      AffineTransform originalTransform = g2d.getTransform();

      if(needsTransform()) {
        //Strange, why it works only when translate followed
        //by rotate and not the other way round ?????
        //Translate link from origin to location defined
        g2d.translate(x1,y1);

        //Get the angle of rotation
        double theta = getRotation(x1,y1,x2,y2);
        g2d.rotate(-theta);
      }

      //Set to solid line
      g2d.setStroke(getDefaultStroke());

      //Draw the link
      super.draw(g2d);

      //Reset to original transformation
      g2d.setTransform(originalTransform);

      //Draw text
      Point midPt = getLineMidPoint(new Point(x1,y1), new Point(x2,y2));
      drawText(g2d, midPt);
    }
  }

  public Point getLineMidPoint(Point p1, Point p2){
    Point midPt = new Point();

    midPt.x = Math.round(p1.x + (p2.x - p1.x)/2.0f);
    midPt.y = Math.round(p1.y + (p2.y - p1.y)/2.0f);

    return midPt;
  }

  private TextLayout initText(Graphics2D g2d, Point pt, String text){
    TextLayout layout = new TextLayout(text, DEFAULT_FONT, g2d.getFontRenderContext());

    //Paint opaque backgrd so that text can be seen clearly
    Rectangle2D bounds = layout.getBounds();
    bounds.setRect(bounds.getX()+pt.x, bounds.getY()+pt.y, bounds.getWidth(), bounds.getHeight());
    Color foregrdColor = g2d.getColor();
    Color bkgrdColor = g2d.getBackground();
    g2d.setColor(bkgrdColor);
    g2d.fill(bounds);
    g2d.setColor(foregrdColor);

    return layout;
  }

  abstract public void drawText(Graphics2D g2d, Point pt); /*{
    //Display stereo type
    if (!getStereoType().equals("")) {
      Point newPt = new Point(pt.x-(int)(getStringWidth(g2d,this.currentFont, getStereoType())/2.0f), pt.y-getFontHeight(g2d,this.currentFont));
      initText(g2d,newPt,getStereoType()).draw(g2d,newPt.x,newPt.y);
    }

    //Display text
    if ((this.linkLabel!=null)&&(this.linkLabel.length()>0)) {
      Point newPt = new Point(pt.x-(int)(getStringWidth(g2d,this.currentFont,linkLabel)/2.0f), pt.y);
      initText(g2d,newPt,linkLabel).draw(g2d,newPt.x,newPt.y);
    }

    //Display the cardinality at the end node
    int offset = (x2 > x1) ?
        -getStringWidth(g2d,this.currentFont,getCardinalityAtEndNode()) :
          getStringWidth(g2d,this.currentFont,getCardinalityAtEndNode());
    
    if (!getCardinalityAtEndNode().equals("")) {
      initText(g2d,new Point(x2+offset,y2),getCardinalityAtEndNode()).
      draw(g2d,x2+offset,y2);
    }

    //Display the cardinality at the start node
    offset = (x2 > x1) ?
        -getStringWidth(g2d,this.currentFont,getCardinalityAtStartNode()) :
          getStringWidth(g2d,this.currentFont,getCardinalityAtStartNode());
    if (!getCardinalityAtStartNode().equals("")) {
      initText(g2d,new Point(x1-offset,y1),getCardinalityAtStartNode()).
      draw(g2d,x1-offset,y1);
    }
    
    g2d.drawString("Mahoganii", pt.x, pt.y-10);
  }*/

    @Override
  public void resize (DrawPanel drawPanel, Point p){
    //Recreate link
    createTmpLink(x1, y1, p.x, p.y);

    /* Note : To optimize the repaint, maybe we could paint a certain
	     rectangle area only.  For example the bounds of the link */

    //Repaint
    drawPanel.repaint();
  }

    @Override
  public void highlight(DrawPanel drawPanel) { }

    @Override
  public void highlight(Graphics g){
    //Number of handles for link
    //int numHandles = 2;

    //Handle width and height
    int handleSize = 4;

    //Handle offset from point
    //no offset at the moment coz it does not actually
    // look nice with it
    //Improve the following algo to position the selection handle
    int handleXoffset = 0;

    //Create necessary selection handles
    selectionHandles = new SelectionHandle[2];

    //Positioning of selection handle at a offset from actual
		//start and end pts
    if (x1 == x2){
      handleXoffset = 0;
    }else if (x1 > x2){  //start point on right of end point
      handleXoffset = -handleXoffset;
    }

    //Start point selection handle
    selectionHandles[0]=new SelectionHandle(x1-handleXoffset,y1,
        handleSize,handleSize, new CaseCursor(Cursor.MOVE_CURSOR,
            CaseCursor.MOVE_CURSOR_START));

    //End point selection handle
    selectionHandles[1]=new SelectionHandle(x2+handleXoffset,y2,
        handleSize,handleSize, new CaseCursor(Cursor.MOVE_CURSOR,
            CaseCursor.MOVE_CURSOR_END));

    //Draw Selection Handles
    for (int i=0;i<selectionHandles.length;i++){
      g.fillRect(selectionHandles[i].getRect().x,
          selectionHandles[i].getRect().y,
          selectionHandles[i].getRect().width,
          selectionHandles[i].getRect().height);
    }
  }

    @Override
  public void setHandleCursor (DrawPanel drawPanel, Point p){
    if (selectionHandles != null){
      for (int i = 0; i < selectionHandles.length; i++){
        if (selectionHandles[i].getRect().contains(p)){
          drawPanel.setCursor(selectionHandles[i].getCursor());
          return;
        }
      }
    }
    drawPanel.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
  }

    @Override
  public void dehighlight (DrawPanel drawPanel){
    Graphics g = drawPanel.getGraphics();
    g.setXORMode(Color.white);
    for(int iCount=0;iCount<selectionHandles.length;iCount++){
      g.fillRect(selectionHandles[iCount].getRect().x,
          selectionHandles[iCount].getRect().y,
          selectionHandles[iCount].getRect().width,
          selectionHandles[iCount].getRect().height);
    }
    g.setPaintMode();
  }

  public void resizeWithStartAnchor(DrawPanel drawPanel, Point p){
    Graphics g =drawPanel.getGraphics();
    if (g != null){
      //Clear old link
      g.setXORMode(Color.white);
      draw(drawPanel);

      //Recreate link
      createTmpLink(x1,y1,p.x,p.y);

      //Draw new link
      g.setPaintMode();
      draw(drawPanel);
    }

    //Repaint
    drawPanel.repaint();
  }

    @Override
  public void resize(DrawPanel drawPanel, Point p, Cursor cursor) {
    if (cursor instanceof CaseCursor){
      //Check which selection handle is clicked to decide
      //start/end point
      if (((CaseCursor)cursor).getCaseType() ==	CaseCursor.MOVE_CURSOR_START){
        dehighlight(drawPanel);
        resizeWithStartAnchor(drawPanel, p);
        highlight(drawPanel);
      }
      //else if it is already the the end point
      else if (((CaseCursor)cursor).getCaseType() == CaseCursor.MOVE_CURSOR_END){
        dehighlight(drawPanel);
        resize(drawPanel, p);
        highlight(drawPanel);
      }
      else
        System.out.println("No such case cursor");
    }
    else
      System.out.println("Not instance of case cursor");
  }

    @Override
  public void setMoveAnchor (Point p){
    this.moveAnchor = p;
  }

  /** Move will move the whole link from one position to another
      with the new position's mid-point = p
  */
    @Override
  public void move(DrawPanel drawPanel, Point p){
    Graphics g = drawPanel.getGraphics();
    if (g != null){
      dehighlight(drawPanel);
      g.setXORMode(Color.white);

      draw(drawPanel);

      //Recompute link anchor positions
      this.x1 = this.x1 + (p.x - moveAnchor.x);
      this.y1 = this.y1 + (p.y - moveAnchor.y);
      this.x2 = this.x2 + (p.x - moveAnchor.x);
      this.y2 = this.y2 + (p.y - moveAnchor.y);

      //Recreate link with new positions
      createLink(this.x1, this.y1, this.x2, this.y2);

      //Reset move anchor to new position
      setMoveAnchor(p);

      g.setPaintMode();

      draw(drawPanel);
      highlight(drawPanel);
    }
    drawPanel.repaint();
  }

  public BasicStroke getDefaultStroke(){
    //Default is a square pen with a linewidth of 1, no dashing,
    // miter segment joins
    // and non-projecting end caps.
    //ie. (1.0f, CAP_SQUARE, JOIN_MITER, 10.0f, null, 0.0f);
    BasicStroke defaultStroke = new BasicStroke();
    return defaultStroke;
  }

  public Stroke getCurrentStroke(){
    return currentStroke;
  }

  public void setCurrentStroke(BasicStroke curStroke){
    currentStroke = curStroke;
  }

  public void unconfirmLink(){
    linkConfirmed = false;
  }

  public void confirmLink(Point startPoint, Point endPoint){
    linkConfirmed = true;

    //Redefines link upon confirmation
    createLink(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
  }

  public boolean isConfirmed(){
    return linkConfirmed;
  }

  public Point getEdgePoint(CaseNode node1, CaseNode node2){
    if ((node1 == null) || (node2 == null)){
      System.err.println("getEdgePoint: node1 or node2 is null");
      return null;
    }

    //Edge Point
    Point edgePoint = new Point();

    //Compute centriods
    Point centriod1 = node1.getCentriod();
    Point centriod2 = node2.getCentriod();

    Point corner = null;
    int width = node1.getWidth();
    int height = node1.getHeight();

    //Determine "nearest corner" of node1 ie corner nearest
	  // to the centriod of node2

    //Check for "direct" cases
    if (centriod2.x == centriod1.x){
      edgePoint.x = centriod1.x;
      if (centriod2.y < centriod1.y)  //directly above
        edgePoint.y = Math.round(centriod1.y - height/2);
      else  //directly below
        edgePoint.y = Math.round(centriod1.y + height/2);

      return edgePoint;
    }

    if (centriod2.y == centriod1.y){
      edgePoint.y = centriod1.y;
      if (centriod2.x < centriod1.x)  //directly on the left
        edgePoint.x = Math.round(centriod1.x - width/2);
      else    //directly on right
        edgePoint.x = Math.round(centriod1.x + width/2);

      return edgePoint;
    }

    //Deal with "non-direct" cases
    if ((centriod2.x>centriod1.x)&&(centriod2.y<centriod1.y))
      //1st quad
      corner = new Point(node1.getStartX()+width, node1.getStartY());
    //upper right corner
    else if ((centriod2.x<centriod1.x)&&(centriod2.y<centriod1.y))
      //2nd quad
      corner = new Point(node1.getStartX(),node1.getStartY());
    //upper left corner
    else if ((centriod2.x<centriod1.x)&&(centriod2.y>centriod1.y))
      //3rd quad
      corner=new Point(node1.getStartX(), node1.getStartY()+height);
    //lower left corner
    else if ((centriod2.x>centriod1.x)&&(centriod2.y>centriod1.y))
      //4th quad
      corner = new Point(node1.getStartX()+width, node1.getStartY() +height);
    //lower right corner
    else
      System.out.println("Unknown condition !!!");

    //Compute tangent alpha and tangent beta
    float tan_alpha = (float)(corner.y-centriod1.y)/(corner.x-centriod1.x);
    float tan_beta = (float)(centriod2.y-centriod1.y)/(centriod2.x-centriod1.x);

    //Determine cutting side
    //When it cuts at the corner, treat it as cutting on the width.
    //The computation is same anyway.
    if (Math.abs(tan_beta) >= Math.abs(tan_alpha)){  //cutting on width side
      int deltaY = corner.y-centriod1.y;
      float deltaX =  deltaY / tan_beta;

      edgePoint.y = corner.y;
      edgePoint.x = Math.round(centriod1.x + deltaX);
    }else if (Math.abs(tan_beta) < Math.abs(tan_alpha)){  //cutting on height side
      int deltaX = corner.x-centriod1.x;
      float deltaY = deltaX * tan_beta;

      edgePoint.x = corner.x;
      edgePoint.y = Math.round(centriod1.y + deltaY);
    }

    return edgePoint;
  }//end getEdgePoint

  public void setCardinalityAtStartNode(String cardinalityAtStartNode) {
    this.cardinalityAtStartNode = cardinalityAtStartNode;
  }

  public void setCardinalityAtEndNode(String cardinalityAtEndNode) {
    this.cardinalityAtEndNode = cardinalityAtEndNode;
  }

  public String getCardinalityAtStartNode() {
    return cardinalityAtStartNode;
  }

  public String getCardinalityAtEndNode() {
    return cardinalityAtEndNode;
  }

	protected boolean checkAttributes(CaseNode nodeAnchored, CaseNode nodeEnded){
    //normally this function should be overrided
    return true;
  }

  /**
   * The reason why this method does not immediately delete the links from
   * the nodes is because the repsonsibility of deletion should be that
   * of the nodes and not the links. The link should only simply inform
   * nodes that it is being deleted. As of what the node would want to do,
   * it is the node's business. The link should not force it to delete
   * though for the moment, the node will going to delete the link.
  */
  public void notifyDeletion(){
    //Notify the nodes anchored that this link is deleted
        getStartNode().linkRemoved(this);
        getEndNode().linkRemoved(this);
  }

  @Override
  public Object clone(){
    //Note : This clone will duplicate the nodes
    // variables copying the references of the
    // connectedLinks references

    //Shallow copy the link
    CaseLink link = (CaseLink)super.clone();
    return link;
  } //end clone

  public void enterData(CaseEditor caseEditor, DrawPanel drawPanel) {
    //Modify input dialog to prompt with the current label data
    if (linkDialog == null){
      linkDialog = new WADLinkDialog(caseEditor, this);
      //linkDialog = new CaseLinkDialogOld();
      linkDialog.setVisible(true);
    }
    else{
      linkDialog.setVisible(true);
    }

    /*
    linkLabel = linkDialog.getLabel();
    setCardinalityAtStartNode(linkDialog.getCardinalityAtStartNode());
    setCardinalityAtEndNode(linkDialog.getCardinalityAtEndNode());
    setStereoType(linkDialog.getStereoType());
    */

    drawPanel.repaint();
  }

    /**
     * @return the linkLabel
     */
    public String getLinkLabel() {
        return linkLabel;
    }

    /**
     * @param linkLabel the linkLabel to set
     */
    public void setLinkLabel(String linkLabel) {
        this.linkLabel = linkLabel;
    }

    public CaseNode getStartNode() {
        return startNode;
    }

    /**
     * @param startNode the startNode to set
     */
    public void setStartNode(CaseNode startNode) {
        this.startNode = startNode;
    }

    public CaseNode getEndNode(){
        return endNode;
    }

    /**
     * @param endNode the endNode to set
     */
    public void setEndNode(CaseNode endNode) {
        this.endNode = endNode;
    }
  
} //end class CaseLink
